<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * This file contains the XML_Feed_RSS2, XML_Feed_RSS2_Channel, and
 * XML_Feed_RSS2_Item classes.
 *
 * PHP version 5
 *
 * This file is part of PEAR::XML_Feed.
 *
 * PEAR::XML_Feed is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * PEAR::XML_Feed is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with PEAR::XML_Feed. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package     XML_Feed
 * @category    XML
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @copyright   2007-2008 Michael J. I. Jackson
 * @license     http://www.gnu.org/licenses/lgpl-3.0.txt GNU LGPL 3.0
 * @version     SVN: $Id: RSS2.php 6 2008-06-30 04:40:59Z mjijackson $
 */

require_once 'XML/Feed.php';

if (!defined('DATE_RSS')) {
    define('DATE_RSS', 'D, d M Y H:i:s O'); // Mon, 15 Aug 2005 15:52:01 +0000
}

/**
 * A class for generating RSS feeds (version 2). This class and its related
 * classes are based on the RSS 2 specification located at the
 * {@link http://www.rssboard.org/rss-specification RSS Advisory Board}.
 *
 * @package     XML_Feed
 * @category    XML
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class XML_Feed_RSS2 extends XML_Feed
{

    /**
     * Constructor.
     *
     * @access  public
     */
    public function __construct()
    {
        parent::__construct();

        $rss = $this->appendChild($this->createElement('rss'));
        $rss->setAttribute('version', '2.0');
    }

    /**
     * Creates a channel and appends it to this feed.
     *
     * @param   string  $title          The title of the channel
     * @param   string  $description    The channel's description
     * @param   string  $link           The channel's URL
     * @return  XML_Feed_RSS2_Channel   The new <channel> node
     * @access  public
     */
    public function addChannel($title, $link, $description)
    {
        return new XML_Feed_RSS2_Channel($this, $title, $link, $description);
    }

}

/**
 * Represents a channel in an RSS2 feed. The only element in the RSS2
 * specification that this class does not support is textInput because it is
 * ignored by most aggregators.
 *
 * @package     XML_Feed
 * @category    XML
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class XML_Feed_RSS2_Channel extends XML_Feed_Element
{

    /**
     * The feed this channel belongs to.
     *
     * @var     XML_Feed_RSS2
     * @access  protected
     */
    protected $feed;

    /**
     * Constructor.
     *
     * @param   XML_Feed_RSS2   $feed           This channel's parent feed
     * @param   string          $title          This channel's title
     * @param   string          $link           This channel's URL
     * @param   string          $description    This channel's description
     * @access  public
     */
    public function __construct(XML_Feed_RSS2 $feed, $title, $link, $description)
    {
        parent::__construct('channel');

        $this->feed = $feed;
        $this->feed->appendChild($this);

        $this->appendChild($feed->createElement('title', $title));
        $this->appendChild($feed->createElement('link', $link));
        $this->appendChild($feed->createElement('description', $description));
    }

    /**
     * Gets this channel's feed.
     *
     * @return  XML_Feed_RSS2       This channel's feed object
     * @access  public
     */
    public function feed()
    {
        return $this->feed;
    }

    /**
     * Appends a <language> element to this channel.
     *
     * @param   string  $lang       The channel's language (i.e. 'en-us')
     * @return  DOMElement          The appended <language> element
     * @access  public
     */
    public function addLanguage($lang)
    {
        return $this->appendChild($this->feed->createElement('language', $lang));
    }

    /**
     * Appends a <copyright> element to this channel.
     *
     * @param   string  $copy       The channel's copyright information
     * @return  DOMElement          The appended <copyright> element
     * @access  public
     */
    public function addCopyright($copy)
    {
        return $this->appendChild($this->feed->createElement('copyright', $copy));
    }

    /**
     * Appends a <managingEditor> element to this channel.
     *
     * @param   string  $editor     The email address of the person responsible
     *                              for this channel's editorial content
     * @return  DOMElement          The appended <managingEditor> element
     * @access  public
     */
    public function addManagingEditor($editor)
    {
        return $this->appendChild($this->feed->createElement('managingEditor', $editor));
    }

    /**
     * Appends a <webMaster> element to this channel.
     *
     * @param   string  $wm         The email address of the person responsible
     *                              for technical issues related to this channel
     * @return  DOMElement          The appended <webMaster> element
     * @access  public
     */
    public function addWebmaster($wm)
    {
        return $this->appendChild($this->feed->createElement('webMaster', $wm));
    }

    /**
     * Appends a <pubDate> element to this channel. If $date is numeric,
     * it will be treated as a timestamp (in seconds) and will be converted
     * to the appropriate format.
     *
     * @param   string  $date       The publication date for the channel's
     *                              content
     * @return  DOMElement          The appended <pubDate> element
     * @access  public
     */
    public function addPubDate($date)
    {
        if (is_numeric($date)) {
            $date = date(DATE_RSS, $date);
        }

        return $this->appendChild($this->feed->createElement('pubDate', $date));
    }

    /**
     * Appends a <lastBuildDate> element to this channel. If $date is
     * numeric, it will be treated as a timestamp (in seconds) and will be
     * converted to the appropriate format.
     *
     * @param   string  $date       The last time the content of the channel
     *                              changed
     * @return  DOMElement          The appended <lastBuildDate> element
     * @access  public
     */
    public function addLastBuildDate($date)
    {
        if (is_numeric($date)) {
            $date = date(DATE_RSS, $date);
        }

        return $this->appendChild($this->feed->createElement('lastBuildDate', $date));
    }

    /**
     * Appends a <category> element to this channel. <category> elements may
     * have an attribute, 'domain', that identifies a categorization taxonomy.
     * The value of the element is a forward-slash-separated string that
     * identifies a hierarchic location in the indicated taxonomy. Processors
     * may establish conventions for the interpretation of categories.
     *
     * @param   string  $category   The channel's category information
     * @param   string  $domain     The categorization taxonomy
     * @return  DOMElement          The appended <category> element
     * @access  public
     */
    public function addCategory($category, $domain = null)
    {
        $category = $this->feed->createElement('category', $category);
        if ($domain) {
            $category->setAttribute('domain', $domain);
        }

        return $this->appendChild($category);
    }

    /**
     * Appends a <generator> element to this channel.
     *
     * @param   string      $generator  The channel's generator information
     * @return  DOMElement              The appended <generator> element
     * @access  public
     */
    public function addGenerator($generator)
    {
        return $this->appendChild($this->feed->createElement('generator', $generator));
    }

    /**
     * Appends a <docs> element to this channel.
     *
     * @param   string  $docs       A URL to the channel's format documentation
     *                              information
     * @return  DOMElement          The appended <docs> element
     * @access  public
     */
    public function addDocs($docs = null)
    {
        if (is_null($docs)) {
            $docs = 'http://www.rssboard.org/rss-specification';
        }

        return $this->appendChild($this->feed->createElement('docs', $docs));
    }

    /**
     * Appends a <cloud> element to this channel. A <cloud> element specifies a
     * web service that supports the rssCloud interface which can be implemented
     * in HTTP-POST, XML-RPC or SOAP 1.1. Its purpose is to allow processes to
     * register with a cloud to be notified of updates to the channel,
     * implementing a lightweight publish-subscribe protocol for RSS feeds.
     *
     * @param   string  $domain             The web service domain
     * @param   int     $port               The port on which the web service is
     *                                      to be contacted
     * @param   string  $path               The web service path
     * @param   string  $registerProcedure  The web service procedure call
     * @param   string  $protocol           The protocol to use with the service
     * @return  DOMElement                  The appended <cloud> element
     * @access  public
     */
    public function addCloud($domain, $port, $path, $registerProcedure, $protocol)
    {
        $cloud = $this->feed->createElement('cloud');
        $cloud->setAttribute('domain', $domain);
        $cloud->setAttribute('port', $port);
        $cloud->setAttribute('path', $path);
        $cloud->setAttribute('registerProcedure', $registerProcedure);
        $cloud->setAttribute('protocol', $protocol);

        return $this->appendChild($cloud);
    }

    /**
     * Appends a <ttl> element to this channel. The $ttl parameter is a number
     * of minutes that indicates how long the channel can be cached before
     * refreshing from the source.
     *
     * @param   int     $ttl        The time to live (in seconds) of this
     *                              channel
     * @return  DOMElement          The appended <ttl> element
     * @access  public
     */
    public function addTTL($ttl)
    {
        return $this->appendChild($this->feed->createElement('ttl', $ttl));
    }

    /**
     * Appends an <image> element to this channel. The <image> element specifies
     * a GIF, JPEG or PNG image that can be displayed with the channel. Note: In
     * practice the image <title> and <link> should have the same value as the
     * channel's <title> and <link>.
     *
     * @param   string  $url            The image url
     * @param   string  $title          An image description to be used in the
     *                                  ALT attribute of the <img> tag when
     *                                  rendered as HTML
     * @param   string  $link           The URL of the channel's site
     * @param   int     $width          The width of the image
     * @param   int     $height         The height of the image
     * @param   string  $description    The link description to be used in the
     *                                  TITLE attribute of the link when
     *                                  rendered as HTML
     * @return  DOMElement              The appended <image> element
     * @access  public
     */
    public function addImage($url, $title, $link, $width = 88, $height = 31,
        $description = null)
    {
        $image = $this->feed->createElement('image');
        $image->appendChild($this->feed->createElement('url', $url));
        $image->appendChild($this->feed->createElement('title', $title));
        $image->appendChild($this->feed->createElement('link', $link));
        $image->appendChild($this->feed->createElement('width', min($width, 144)));
        $image->appendChild($this->feed->createElement('height', min($height, 400)));
        if ($description) {
            $image->appendChild($this->feed->createElement('description', $description));
        }

        return $this->appendChild($image);
    }

    /**
     * Appends a <rating> element to this channel. See the
     * {@link http://www.w3.org/PICS/ PICS specification} for more information.
     *
     * @param   string      $rating     The channel's PICS rating information
     * @return  DOMElement              The appended <rating> element
     * @access  public
     */
    public function addRating($rating)
    {
        return $this->appendChild($this->feed->createElement('rating', $rating));
    }

    /**
     * Appends a <skipHours> element to this channel. The $hours parameter
     * should be an array of numbers (0-23) that specify GMT hours during which
     * an aggregator may not read the channel. Hour 0 is midnight.
     *
     * @param   array       $hours      The channel's skip hours information
     * @return  DOMElement              The appended <skipHours> element
     * @access  public
     */
    public function addSkipHours($hours)
    {
        $skipHours = $this->feed->createElement('skipHours');
        foreach ($hours as $hour) {
            if ($hour >= 0 && $hour <= 23) {
                $skipHours->appendChild($this->feed->createElement('hour', $hour));
            }
        }

        return $this->appendChild($this->feed->createElement('skipHours', $skipHours));
    }

    /**
     * Appends a <skipDays> element to this channel. The $days parameter
     * should be an array of strings (Sunday-Saturday) that specify days on
     * which aggregators may not read this channel.
     *
     * @param   array       $days       The channel's skip days information
     * @return  DOMElement              The appended <skipDays> element
     * @access  public
     */
    public function addSkipDays($days)
    {
        $skipDays = $this->feed->createElement('skipDays');
        foreach ($days as $day) {
            $skipDays->appendChild($this->feed->createElement('day', $day));
        }

        return $this->appendChild($this->feed->createElement('skipDays', $skipDays));
    }

    /**
     * Adds an item to this feed.
     *
     * @param   string              $title          The item's title
     * @param   string              $description    The item's description
     * @param   string              $link           The item's link
     * @return  XML_Feed_RSS2_Item                  The appended <item> node
     * @access  public
     */
    public function addItem($title = null, $description = null, $link = null)
    {
        return new XML_Feed_RSS2_Item($this, $title, $description, $link);
    }

}

/**
 * Represents an item in an RSS2 feed channel.
 *
 * @package     XML_Feed
 * @category    XML
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 */
class XML_Feed_RSS2_Item extends XML_Feed_Element
{

    /**
     * The channel this item belongs to.
     *
     * @var     XML_Feed_RSS2_Channel
     * @access  protected
     */
    protected $channel;

    /**
     * Constructor.
     *
     * @param   XML_Feed_RSS2_Channel   $channel        This item's parent
     *                                                  channel
     * @param   string                  $title          This item's title
     * @param   string                  $description    This item's description
     * @param   string                  $link           This item's link
     * @throws  XML_Feed_Exception                      If both the title and
     *                                                  description are null
     * @access  public
     */
    public function __construct(XML_Feed_RSS2_Channel $channel, $title = null,
        $description = null, $link = null)
    {
        parent::__construct('item');

        if (is_null($title) && is_null($description)) {
            throw new XML_Feed_Exception('An RSS2 feed item must have a title or description');
        }

        $this->channel = $channel;
        $this->channel->appendChild($this);

        $feed = $this->feed();
        if (!is_null($title)) {
            $this->appendChild($feed->createElement('title', $title));
        }
        if (!is_null($description)) {
            $this->appendChild($feed->createElement('description', $description));
        }
        if (!is_null($link)) {
            $this->appendChild($feed->createElement('link', $link));
        }
    }

    /**
     * Gets this item's feed.
     *
     * @return  XML_Feed_RSS2       This item's feed
     * @access  public
     */
    public function feed()
    {
        return $this->channel->feed();
    }

    /**
     * Appends an <author> element to this item.
     *
     * @param   string  $author     The email address of this item's author
     * @return  DOMElement          The appended <author> element
     * @access  public
     */
    public function addAuthor($author)
    {
        $feed = $this->feed();
        return $this->appendChild($feed->createElement('author', $author));
    }

    /**
     * Appends a <category> element to this item. <category> elements may
     * have an attribute, 'domain', that identifies a categorization taxonomy.
     * The value of the element is a forward-slash-separated string that
     * identifies a hierarchic location in the indicated taxonomy. Processors
     * may establish conventions for the interpretation of categories.
     *
     * @param   string  $category   The item's category information
     * @param   string  $domain     The categorization taxonomy
     * @return  DOMElement          The appended <category> element
     * @access  public
     */
    public function addCategory($category, $domain = null)
    {
        $feed = $this->feed();
        $category = $feed->createElement('category', $category);
        if ($domain) {
            $category->setAttribute('domain', $domain);
        }

        return $this->appendChild($category);
    }

    /**
     * Appends a <comments> element to this item.
     *
     * @param   string  $comments   The URL of a comments page for this item
     * @return  DOMElement          The appended <comments> element
     * @access  public
     */
    public function addComments($comments)
    {
        $feed = $this->feed();
        return $this->appendChild($feed->createElement('comments', $comments));
    }

    /**
     * Appends an <enclosure> element to this item.
     *
     * @param   string  $url        The URL of the enclosure
     * @param   int     $length     The length of the enclosure file in bytes
     * @param   string  $type       The mime-type of the enclosure
     * @return  DOMElement          The appended <enclosure> element
     * @access  public
     */
    public function addEnclosure($url, $length, $type)
    {
        $feed = $this->feed();
        $enclosure = $feed->createElement('enclosure');
        $enclosure->setAttribute('url', $url);
        $enclosure->setAttribute('length', $length);
        $enclosure->setAttribute('type', $type);

        return $this->appendChild($enclosure);
    }

    /**
     * Appends a <guid> element to this item. If the $isPermaLink parameter is
     * true, the reader may assume that the guid is a permalink to the item,
     * that is, a URL that can be opened in a web browser, that points to the
     * full item described by the <item> element.
     *
     * @param   string  $guid           A globally unique identifier for this item
     * @param   bool    $isPermaLink    True if the guid specifies a permalink
     * @return  DOMElement              The appended <guid> element
     * @access  public
     */
    public function addGuid($guid, $isPermaLink = null)
    {
        $feed = $this->feed();
        $guid = $feed->createElement('guid', $guid);
        if (is_bool($isPermaLink)) {
            $guid->setAttribute('isPermaLink', ($isPermaLink ? 'true' : 'false'));
        }

        return $this->appendChild($guid);
    }

    /**
     * Appends a <pubDate> element to this item. If $date is numeric,
     * it will be treated as a timestamp (in seconds) and will be converted
     * to the appropriate format. If this is a date in the future, aggregators
     * may choose to not display the item until that date.
     *
     * @param   string  $date       The publication date for this item
     * @return  DOMElement          The appended <pubDate> element
     * @access  public
     */
    public function addPubDate($date)
    {
        if (is_numeric($date)) {
            $date = date(DATE_RSS, $date);
        }

        $feed = $this->feed();
        return $this->appendChild($feed->createElement('pubDate', $date));
    }

    /**
     * Appends a <source> element to this item. Its value is the name of the RSS
     * channel that the item came from, derived from its <title>. It has one
     * required attribute, url, which links to the XML of the source.
     *
     * @param   string  $name       The name of the source feed
     * @param   string  $url        The URL of the source feed
     * @return  DOMElement          The appended <source> element
     * @access  public
     */
    public function addSource($name, $url)
    {
        $feed = $this->feed();
        $source = $feed->createElement('source', $name);
        $source->setAttribute('url', $url);

        return $this->appendChild($source);
    }

}

?>
